Popular Searches
Popular Course Categories
Popular Courses

Storing Simple Preferences Using DataStore

Storing Simple Preferences Using DataStore

Android Data Storage and Local Databases

Storing Simple Preferences Using DataStore in Android

Course: Android App Development

Topic: Storing Simple Preferences Using DataStore

Course Link: Android App Development Training

Demo Link: Register for a Course Demo

Website: JustAcademy


1. Introduction to DataStore

Android applications often need to remember small pieces of information between app sessions. Examples include a user's preferred theme, language, notification settings, onboarding completion status, or whether a particular feature is enabled. Jetpack DataStore provides a modern way to store this kind of data persistently.

Preferences DataStore stores information as key-value pairs. It uses Kotlin coroutines and Flow to perform asynchronous data operations, helping applications avoid blocking the main UI thread. It is commonly used instead of the older SharedPreferences API for simple application settings.

2. Learning Objectives

  • Understand what Jetpack DataStore is and why it is useful.
  • Differentiate Preferences DataStore from Proto DataStore and SharedPreferences.
  • Add the DataStore dependency to an Android project.
  • Create typed preference keys for different values.
  • Save, read, update, and remove preferences.
  • Use Kotlin Flow and coroutines to observe and update stored data.
  • Integrate preferences into a Jetpack Compose screen.
  • Apply best practices for error handling, architecture, and data security.

3. What Is Preferences DataStore?

Preferences DataStore is part of Android Jetpack. It stores small values using named keys, with each key associated with a supported data type. The data remains available when the application is closed and reopened, unless it is cleared or the app's data is removed.

For example, a settings screen may store the following values:

Preference Data Type Example Value Purpose
Dark theme Boolean true Remember the selected theme.
Username String student01 Remember a non-sensitive display name.
Font size Int 18 Store a preferred text size.
Last sync time Long 1720000000000 Store a timestamp.
Playback speed Float 1.5 Remember a media preference.
Notifications enabled Boolean false Remember a notification setting.

4. Types of DataStore

4.1 Preferences DataStore

Preferences DataStore is suitable for simple key-value settings. You do not need to define a schema or create a data class for every preference. However, it does not provide the same schema-based type safety as Proto DataStore.

4.2 Proto DataStore

Proto DataStore stores structured objects defined through a schema, commonly using Protocol Buffers. It is appropriate when an application needs a well-defined data model and stronger schema-based guarantees.

Feature Preferences DataStore Proto DataStore
Storage model Key-value pairs Structured typed objects
Schema required No Yes, through a defined model/schema
Best suited for Theme, language, toggles, small settings Structured application configuration
Learning complexity Lower Higher

5. DataStore vs SharedPreferences

SharedPreferences is an older Android API for storing key-value pairs. Preferences DataStore provides an asynchronous Flow-based API and transactional updates, making it a strong default choice for new simple-preference implementations.

Feature SharedPreferences Preferences DataStore
Reading data Getter methods and listeners Flow-based data stream
Writing data Editor with apply() or commit() Suspending edit() function
Threading model Some operations can cause main-thread performance issues Designed for asynchronous I/O
Updates Editor-based updates Transactional updates
Error handling Limited error reporting in common usage Can expose storage and I/O errors through Flow
Ideal use Existing legacy implementations Modern small settings and preferences

6. Add the DataStore Dependency

Open the app-level build.gradle.kts file and add the Preferences DataStore library to the dependencies block. Use a version compatible with your project's Android Gradle Plugin and Kotlin setup.

dependencies {
    implementation("androidx.datastore:datastore-preferences:1.3.0")
}

If your project uses Groovy Gradle syntax, write the dependency as follows:

dependencies {
    implementation "androidx.datastore:datastore-preferences:1.3.0"
}

After adding the dependency, sync the project with Gradle. If your project already uses a version catalog, you can declare the library there and reference it from the module dependencies.

7. Create a Preferences DataStore Instance

Create a Kotlin file named AppPreferences.kt. Declare the DataStore delegate at the top level so that the application can reuse a single instance for this file.

package com.example.preferencesdemo

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore

val Context.dataStore: DataStore<Preferences> by
    preferencesDataStore(name = "app_settings")

Explanation:

  • Context.dataStore is an extension property that exposes the DataStore instance.
  • preferencesDataStore() creates a delegate for Preferences DataStore.
  • app_settings is the logical name used for the stored preferences file.
  • The top-level declaration helps avoid accidentally creating multiple DataStore instances for the same file.

Keep the declaration in one shared location rather than declaring a new delegate inside every Activity, ViewModel, or composable.

8. Define Preference Keys

A preference key identifies a stored value. Choose the key factory that matches the type of value you need to save.

import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey

object PreferenceKeys {
    val USERNAME = stringPreferencesKey("username")
    val DARK_MODE = booleanPreferencesKey("dark_mode")
    val FONT_SIZE = intPreferencesKey("font_size")
    val LAST_SYNC_TIME = longPreferencesKey("last_sync_time")
}
Key Factory Value Type Typical Use
stringPreferencesKey() String Language code or display name
booleanPreferencesKey() Boolean Theme or notification toggle
intPreferencesKey() Int Font size or selected tab index
longPreferencesKey() Long Timestamp or large counter
floatPreferencesKey() Float Volume or playback preference
doublePreferencesKey() Double Decimal-valued setting
stringSetPreferencesKey() Set<String> A small collection of selected options

9. Save a Simple Preference

To save a preference, use the suspending edit() function. It provides a mutable preferences object in which values can be added or changed.

import android.content.Context
import androidx.datastore.preferences.core.edit

suspend fun saveUsername(context: Context, username: String) {
    context.dataStore.edit { preferences ->
        preferences[PreferenceKeys.USERNAME] = username
    }
}

Because edit() is a suspending function, call it from a coroutine or another suspending function. Do not block the main thread with a synchronous wait.

10. Save Multiple Preferences

You can update several related settings inside a single edit transaction.

suspend fun saveAppSettings(
    context: Context,
    username: String,
    darkMode: Boolean,
    fontSize: Int
) {
    context.dataStore.edit { preferences ->
        preferences[PreferenceKeys.USERNAME] = username
        preferences[PreferenceKeys.DARK_MODE] = darkMode
        preferences[PreferenceKeys.FONT_SIZE] = fontSize
    }
}

Grouping related changes in one transaction helps prevent observers from seeing intermediate states between separate writes.

11. Read Preferences Using Flow

DataStore exposes its stored values through a Flow. A Flow can emit updated values as preferences change, allowing the UI or ViewModel to react to updates.

import android.content.Context
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

fun usernameFlow(context: Context): Flow<String> =
    context.dataStore.data.map { preferences ->
        preferences[PreferenceKeys.USERNAME] ?: "Guest"
    }

The expression ?: "Guest" provides a default when the username has not been saved yet. Similar defaults can be used for other preferences.

fun darkModeFlow(context: Context): Flow<Boolean> =
    context.dataStore.data.map { preferences ->
        preferences[PreferenceKeys.DARK_MODE] ?: false
    }

fun fontSizeFlow(context: Context): Flow<Int> =
    context.dataStore.data.map { preferences ->
        preferences[PreferenceKeys.FONT_SIZE] ?: 16
    }

12. Update an Existing Preference

Saving a value with an existing key replaces the previous value for that key.

suspend fun updateDarkMode(
    context: Context,
    enabled: Boolean
) {
    context.dataStore.edit { preferences ->
        preferences[PreferenceKeys.DARK_MODE] = enabled
    }
}

For example, if dark mode was previously set to false, calling this function with true updates the stored preference. A collector of the corresponding Flow can receive the new value.

13. Remove a Preference

Use remove() to delete one key from the stored preferences without deleting the other settings.

suspend fun removeUsername(context: Context) {
    context.dataStore.edit { preferences ->
        preferences.remove(PreferenceKeys.USERNAME)
    }
}

To clear every preference in this DataStore file, use clear(). Be careful because this removes all values in the file, not just one setting.

suspend fun clearAllPreferences(context: Context) {
    context.dataStore.edit { preferences ->
        preferences.clear()
    }
}

14. Handle DataStore Errors

DataStore can report I/O problems when reading from storage. A Flow can use the catch operator to handle expected I/O exceptions and provide a fallback value where appropriate.

import androidx.datastore.core.IOException
import androidx.datastore.preferences.core.emptyPreferences
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

fun safeDarkModeFlow(context: Context) =
    context.dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }
        .map { preferences ->
            preferences[PreferenceKeys.DARK_MODE] ?: false
        }

If your IDE does not resolve IOException from the DataStore package, use java.io.IOException instead, as commonly shown in DataStore examples. Do not silently suppress every exception: unexpected errors should be allowed to propagate or be handled intentionally by the application's error-reporting strategy.

15. Use DataStore with a ViewModel

In a well-structured Android application, storage operations belong in a repository or data layer. The ViewModel exposes observable state to the UI, while the composable screen displays that state and sends user actions to the ViewModel.

15.1 Create a Repository

import android.content.Context
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class UserPreferencesRepository(
    private val context: Context
) {
    val darkMode: Flow<Boolean> =
        context.dataStore.data.map { preferences ->
            preferences[PreferenceKeys.DARK_MODE] ?: false
        }

    suspend fun setDarkMode(enabled: Boolean) {
        context.dataStore.edit { preferences ->
            preferences[PreferenceKeys.DARK_MODE] = enabled
        }
    }
}

15.2 Create a ViewModel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

class SettingsViewModel(
    private val repository: UserPreferencesRepository
) : ViewModel() {

    val darkMode = repository.darkMode.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = false
    )

    fun setDarkMode(enabled: Boolean) {
        viewModelScope.launch {
            repository.setDarkMode(enabled)
        }
    }
}

This example assumes the ViewModel receives its repository through dependency injection or a ViewModel factory. A production app should configure that construction explicitly instead of expecting Android to instantiate a ViewModel with arbitrary constructor parameters automatically.

16. Display and Update Preferences in Jetpack Compose

Jetpack Compose can observe the ViewModel's state and display a switch for a saved dark-mode preference. Add the lifecycle Compose integration dependency if it is not already included in your project.

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle

@Composable
fun SettingsScreen(viewModel: SettingsViewModel) {
    val darkMode by viewModel.darkMode.collectAsStateWithLifecycle()

    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Dark Mode",
            style = MaterialTheme.typography.titleLarge
        )

        Switch(
            checked = darkMode,
            onCheckedChange = { enabled ->
                viewModel.setDarkMode(enabled)
            }
        )
    }
}

When the user changes the switch, the ViewModel requests an update. DataStore persists the new value, and the Flow emits the updated state. Compose then recomposes the relevant UI. For a complete theme implementation, connect the preference to your app's theme at the appropriate root composable.

17. Practical Example: Remembering a Username

Consider a learning application that asks the user for a display name. The app should remember the name after it is closed and opened again.

  1. Define a String key named username.
  2. Save the text entered by the user using dataStore.edit().
  3. Expose the saved name as a Flow.
  4. Provide a default such as Guest when no value exists.
  5. Collect the Flow in a ViewModel and display it in the UI.
  6. Test the result by restarting the app and verifying that the saved name remains available.

This approach works well for non-sensitive display preferences. Do not use a plain preference key as a substitute for secure credential storage.

18. Common Errors and Troubleshooting

Problem Possible Cause Solution
Unresolved DataStore imports Missing dependency or Gradle sync issue Check the module dependency and sync Gradle.
Value returns a default The key has never been saved or a different key name is being used Check the key declaration and the write function.
Write function gives a coroutine error A suspending function was called outside a coroutine Call it from a coroutine scope or another suspending function.
UI does not update Stored Flow is not collected or the UI uses a stale value Observe the Flow through the ViewModel and lifecycle-aware collection.
Multiple DataStore declarations Separate instances are created for the same file Centralize the delegate and reuse the same instance.
Data appears missing after reinstall App data was cleared or the app was reinstalled Understand the lifecycle of local app storage and backups.
Crash when reading data Unexpected I/O or data-related failure Handle expected I/O errors and investigate unexpected exceptions.

19. Best Practices

  • Prefer Preferences DataStore for small settings such as theme, language, and UI toggles.
  • Declare a single DataStore delegate for each file and share it throughout the app.
  • Keep persistence logic in a repository or data layer rather than writing directly inside composables.
  • Use Kotlin Flow to observe stored values and coroutines to perform updates.
  • Supply sensible defaults for preferences that may not have been set.
  • Use descriptive and consistent key names.
  • Group related updates in a single transaction where appropriate.
  • Handle expected I/O failures without hiding unexpected errors.
  • Use Room for relational, large, or complex datasets that need queries and relationships.
  • Do not store passwords, authentication tokens, or other secrets as ordinary Preferences DataStore values. DataStore does not encrypt stored values by default.
  • Test both first-run behavior and behavior after the app has saved preferences.

20. When Should You Use DataStore?

Requirement Recommended Storage Reason
Remember dark mode Preferences DataStore Simple Boolean preference.
Remember selected language Preferences DataStore Simple String preference.
Save a structured settings object Proto DataStore or another suitable typed DataStore Provides a defined data model.
Store thousands of related records Room Supports database queries and relationships.
Store highly sensitive credentials Purpose-built secure credential storage Ordinary DataStore preferences are not encrypted by default.

21. Practice Exercise

Task: Build a Settings screen that remembers a user's preferred language and dark-mode selection.

Requirements:

  1. Create a Preferences DataStore instance named user_settings.
  2. Define a String key for the language and a Boolean key for dark mode.
  3. Write repository functions to read and update both preferences.
  4. Expose the values through a ViewModel.
  5. Build a Compose screen containing a language selector and a dark-mode switch.
  6. Run the app, change both settings, close the app, and reopen it to verify persistence.
  7. Add appropriate fallback values and handle expected I/O errors.

22. Interview Questions and Answers

Q1. What is Jetpack DataStore?

DataStore is an Android Jetpack solution for storing small preferences or structured objects asynchronously and transactionally.

Q2. What is Preferences DataStore?

It is the key-value implementation of DataStore, suitable for simple settings without requiring a predefined schema.

Q3. Why is DataStore often preferred over SharedPreferences?

It provides an asynchronous API based on coroutines and Flow, transactional updates, and a clearer way to propagate storage errors.

Q4. What is the purpose of preferencesDataStore()?

It creates a delegate for accessing a Preferences DataStore instance associated with a named file.

Q5. What does edit() do?

It runs a suspending transaction that provides mutable preferences, allowing keys to be added, changed, or removed.

Q6. Why does DataStore expose a Flow?

A Flow emits stored data over time, allowing consumers to react to preference changes rather than reading only a one-time snapshot.

Q7. How can a default value be provided?

Use the Elvis operator, for example preferences[DARK_MODE] ?: false.

Q8. Can DataStore store complex database records?

It is not designed for large relational datasets. Room is usually a better choice when an app needs database queries, relationships, or partial record updates.

Q9. Is Preferences DataStore encrypted by default?

No. Do not treat it as secure storage for passwords or tokens. Use an appropriate security solution for sensitive information.

Q10. Where should DataStore logic live in a Compose application?

Prefer a repository or data layer accessed by a ViewModel, then collect the exposed state in Compose using lifecycle-aware collection.

23. Quick Revision

  • Preferences DataStore stores small key-value settings.
  • Use typed key factories such as stringPreferencesKey() and booleanPreferencesKey().
  • Use dataStore.edit { } to save, update, or remove values.
  • Use dataStore.data and Flow operators to read preferences reactively.
  • Use coroutines for suspending writes.
  • Keep storage logic in a repository and expose state through a ViewModel.
  • Use lifecycle-aware collection in Jetpack Compose.
  • Choose Room for complex relational data and avoid storing secrets in unencrypted preferences.

24. Official Learning Resources

Conclusion

Preferences DataStore is a useful solution for persisting simple Android application settings such as theme choices, language selections, and UI preferences. By combining typed keys, transactional writes, Kotlin coroutines, and Flow, developers can build settings features that remain available across app sessions and update the UI reactively. For maintainable applications, keep persistence in a repository, expose state through a ViewModel, and use the storage solution that matches the size, structure, and sensitivity of the data.

Explore the course: Android App Development Training | Register for a Demo | JustAcademy Homepage

Previous Next
whatsapp